.. role:: raw-html-m2r(raw) :format: html VGA === Introduction ------------ VGA interfaces are becoming an endangered species, but implementing a VGA controller is still a good exercise. An explanation about the VGA protocol can be found `here `_. This VGA controller tutorial is based on `this `_ implementation. Data structures --------------- Before implementing the controller itself we need to define some data structures. RGB color ^^^^^^^^^ First, we need a three channel color structure (Red, Green, Blue). This data structure will be used to feed the controller with pixels and also will be used by the VGA bus. .. code-block:: scala case class RgbConfig(rWidth : Int,gWidth : Int,bWidth : Int){ def getWidth = rWidth + gWidth + bWidth } case class Rgb(c: RgbConfig) extends Bundle{ val r = UInt(c.rWidth bit) val g = UInt(c.gWidth bit) val b = UInt(c.bWidth bit) } VGA bus ^^^^^^^ .. list-table:: :header-rows: 1 :widths: 1 1 10 * - io name - Driver - Description * - vSync - master - Vertical synchronization, indicate the beginning of a new frame * - hSync - master - Horizontal synchronization, indicate the beginning of a new line * - colorEn - master - High when the interface is in the visible part * - color - master - Carry the color, don't care when colorEn is low .. code-block:: scala case class Vga (rgbConfig: RgbConfig) extends Bundle with IMasterSlave{ val vSync = Bool val hSync = Bool val colorEn = Bool val color = Rgb(rgbConfig) override def asMaster() : Unit = this.asOutput() } This Vga ``Bundle`` uses the ``IMasterSlave`` trait, which allows you to create master/slave VGA interfaces using the following: .. code-block:: text master(Vga(...)) slave(Vga(...)) VGA timings ^^^^^^^^^^^ The VGA interface is driven by using 8 different timings. Here is one simple example of a ``Bundle`` that is able to carry them. .. code-block:: scala case class VgaTimings(timingsWidth: Int) extends Bundle { val hSyncStart = UInt(timingsWidth bits) val hSyncEnd = UInt(timingsWidth bits) val hColorStart = UInt(timingsWidth bits) val hColorEnd = UInt(timingsWidth bits) val vSyncStart = UInt(timingsWidth bits) val vSyncEnd = UInt(timingsWidth bits) val vColorStart = UInt(timingsWidth bits) val vColorEnd = UInt(timingsWidth bits) } But this not a very good way to specify it because it is redundant for vertical and horizontal timings. Let's write it in a clearer way: .. code-block:: scala case class VgaTimingsHV(timingsWidth: Int) extends Bundle { val colorStart = UInt(timingsWidth bit) val colorEnd = UInt(timingsWidth bit) val syncStart = UInt(timingsWidth bit) val syncEnd = UInt(timingsWidth bit) } case class VgaTimings(timingsWidth: Int) extends Bundle { val h = VgaTimingsHV(timingsWidth) val v = VgaTimingsHV(timingsWidth) } Then we could add some some functions to set these timings for specific resolutions and frame rates: .. code-block:: scala case class VgaTimingsHV(timingsWidth: Int) extends Bundle { val colorStart = UInt(timingsWidth bit) val colorEnd = UInt(timingsWidth bit) val syncStart = UInt(timingsWidth bit) val syncEnd = UInt(timingsWidth bit) } case class VgaTimings(timingsWidth: Int) extends Bundle { val h = VgaTimingsHV(timingsWidth) val v = VgaTimingsHV(timingsWidth) def setAs_h640_v480_r60: Unit = { h.syncStart := 96 - 1 h.syncEnd := 800 - 1 h.colorStart := 96 + 16 - 1 h.colorEnd := 800 - 48 - 1 v.syncStart := 2 - 1 v.syncEnd := 525 - 1 v.colorStart := 2 + 10 - 1 v.colorEnd := 525 - 33 - 1 } def setAs_h64_v64_r60: Unit = { h.syncStart := 96 - 1 h.syncEnd := 800 - 1 h.colorStart := 96 + 16 - 1 + 288 h.colorEnd := 800 - 48 - 1 - 288 v.syncStart := 2 - 1 v.syncEnd := 525 - 1 v.colorStart := 2 + 10 - 1 + 208 v.colorEnd := 525 - 33 - 1 - 208 } } VGA Controller -------------- Specification ^^^^^^^^^^^^^ .. list-table:: :header-rows: 1 :widths: 1 1 10 * - io name - Direction - Description * - softReset - in - Reset internal counters and keep the VGA interface inactive * - timings - in - Specify VGA horizontal and vertical timings * - pixels - slave - Stream of RGB colors that feeds the VGA controller * - error - out - High when the pixels stream is too slow * - frameStart - out - High when a new frame starts * - vga - master - VGA interface The controller does not integrate any pixel buffering. It directly takes them from the ``pixels`` ``Stream`` and puts them on the ``vga.color`` out at the right time. If ``pixels`` is not valid then ``error`` becomes high for one cycle. Component and io definition ^^^^^^^^^^^^^^^^^^^^^^^^^^^ Let's define a new VgaCtrl ``Component``\ , which takes as ``RgbConfig`` and ``timingsWidth`` as parameters. Let's give the bit width a default value of 12. .. code-block:: scala class VgaCtrl(rgbConfig: RgbConfig, timingsWidth: Int = 12) extends Component { val io = new Bundle { val softReset = in Bool val timings = in(VgaTimings(timingsWidth)) val pixels = slave Stream (Rgb(rgbConfig)) val error = out Bool val frameStart = out Bool val vga = master(Vga(rgbConfig)) } ... } Horizontal and vertical logic ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The logic that generates horizontal and vertical synchronization signals is quite the same. It kind of resembles ~PWM~. The horizontal one counts up each cycle, while the vertical one use the horizontal syncronization signal as to increment. Let's define ``HVArea``\ , which represents one ~PWM~ and then instantiate it two times: one for both horizontal and vertical syncronization. .. code-block:: scala class VgaCtrl(rgbConfig: RgbConfig, timingsWidth: Int = 12) extends Component { val io = new Bundle {...} case class HVArea(timingsHV: VgaTimingsHV, enable: Bool) extends Area { val counter = Reg(UInt(timingsWidth bit)) init(0) val syncStart = counter === timingsHV.syncStart val syncEnd = counter === timingsHV.syncEnd val colorStart = counter === timingsHV.colorStart val colorEnd = counter === timingsHV.colorEnd when(enable) { counter := counter + 1 when(syncEnd) { counter := 0 } } val sync = RegInit(False) setWhen(syncStart) clearWhen(syncEnd) val colorEn = RegInit(False) setWhen(colorStart) clearWhen(colorEnd) when(io.softReset) { counter := 0 sync := False colorEn := False } } val h = HVArea(io.timings.h, True) val v = HVArea(io.timings.v, h.syncEnd) } As you can see, it's done by using ``Area``. This is to avoid the creation of a new ``Component`` which would have been much more verbose. Interconnections ^^^^^^^^^^^^^^^^ Now that we have timing generators for horizontal and vertical synchronization, we need to drive the outputs. .. code-block:: scala class VgaCtrl(rgbConfig: RgbConfig, timingsWidth: Int = 12) extends Component { val io = new Bundle {...} case class HVArea(timingsHV: VgaTimingsHV, enable: Bool) extends Area {...} val h = HVArea(io.timings.h, True) val v = HVArea(io.timings.v, h.syncEnd) val colorEn = h.colorEn && v.colorEn io.pixels.ready := colorEn io.error := colorEn && ! io.pixels.valid io.frameStart := v.syncEnd io.vga.hSync := h.sync io.vga.vSync := v.sync io.vga.colorEn := colorEn io.vga.color := io.pixels.payload } Bonus ^^^^^ The VgaCtrl that was defined above is generic (not application specific). We can imagine a case where the system provides a ``Stream`` of ``Fragment`` of RGB, which means the system transmits pixels between start/end of picture indications. In this case we can automatically manage the ``softReset`` input by asserting it when an ``error`` occurs, then wait for the end of the current ``pixels`` picture to deassert ``error``. Let's add a function to ``VgaCtrl`` that can be called from the parent component to feed ``VgaCtrl`` by using this ``Stream`` of ``Fragment`` of RGB. .. code-block:: scala class VgaCtrl(rgbConfig: RgbConfig, timingsWidth: Int = 12) extends Component { ... def feedWith(that : Stream[Fragment[Rgb]]): Unit ={ io.pixels << that.toStreamOfFragment val error = RegInit(False) when(io.error){ error := True } when(that.isLast){ error := False } io.softReset := error when(error){ that.ready := True } } }